#!/usr/bin/env bash
# ==============================================================================
# Grey Matter pre-commit gate
# ==============================================================================
# Regenerates directory index tables, runs the fast test subset, and runs
# the OKF §10 attestation. A commit proceeds only when all three are clean.
#
# Index tables and the attestation receipt are deterministic functions of the
# vault's markdown, so they carry no information the commit does not already
# hold. The hook stages them rather than making you hand-crank a build system.
#
# Both are generated from the working tree, so that is only sound when the
# markdown feeding them is already staged. When a commit touches that content
# the hook requires all of it to be staged and refuses otherwise, rather than
# recording tables that describe something the commit does not contain. When a
# commit touches none of it -- an engine or tooling change, with notes left
# half-written in the editor -- there is nothing to keep in step, so the hook
# generates nothing on your behalf and stays out of the way.
# ==============================================================================

set -euo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$REPO_ROOT"

SYSTEM_DIR="99 - System"
BRAIN="$SYSTEM_DIR/brain.py"
RECEIPT="$SYSTEM_DIR/receipts/latest-vault-health.json"

# Exactly what ops._digest_inputs() walks: every markdown file outside the
# excluded directories, plus the attachments. Listing the note directories
# instead would miss the root directives and log, which are excluded from the
# knowledge graph but still feed the attested digest.
DIGEST_PATHS=(
    '*.md'
    '05 - Assets/attachments'
    ':(exclude)99 - System/**'
    ':(exclude).agents/**'
    ':(exclude).obsidian/**'
    ':(exclude).gemini/**'
)

fail() {
    printf '\n\033[31m✗ [pre-commit] %s\033[0m\n' "$1"
    shift
    for line in "$@"; do printf '  %s\n' "$line"; done
    printf '\n'
    exit 1
}

# ---------------------------------------------------------------- precondition
staged=()
while IFS= read -r file; do
    [[ -n "$file" ]] && staged+=("$file")
done < <(git diff --cached --name-only -- "${DIGEST_PATHS[@]}" || true)

generating=0
(( ${#staged[@]} )) && generating=1

if (( generating )); then
    drifted=()
    while IFS= read -r file; do
        [[ -n "$file" ]] && drifted+=("$file")
    done < <(git diff --name-only -- "${DIGEST_PATHS[@]}" || true)
    while IFS= read -r file; do
        [[ -n "$file" ]] && drifted+=("$file")
    done < <(git ls-files --others --exclude-standard -- "${DIGEST_PATHS[@]}" || true)

    if (( ${#drifted[@]} )); then
        printf '\n\033[33m! [pre-commit] This commit changes vault content, but these\n'
        printf '  files are not staged as they stand:\033[0m\n'
        printf '    %s\n' "${drifted[@]}"
        fail "Index tables and attestation would describe content you are not committing." \
             "Stage them so the generated output matches the commit:" \
             "" \
             "    git add -- <the files above>" \
             "" \
             "Or commit them separately first. Use --no-verify only if you mean to" \
             "record generated files that disagree with this commit, which is what" \
             "splitting one working tree across several commits necessarily does."
    fi
fi

# ------------------------------------------------------------------- indexes
printf '→ [pre-commit] Synchronizing index tables...\n'
python3 "$BRAIN" sync-indexes >/dev/null

rewritten=()
while IFS= read -r file; do
    [[ -n "$file" ]] && rewritten+=("$file")
done < <(git diff --name-only -- '*index.md' || true)
while IFS= read -r file; do
    [[ -n "$file" ]] && rewritten+=("$file")
done < <(git ls-files --others --exclude-standard -- '*index.md' || true)

if (( ${#rewritten[@]} )); then
    printf '\033[33m  ! index tables were out of date and have been rewritten\033[0m\n'
    printf '    %s\n' "${rewritten[@]}"
    if (( generating )); then
        # The precondition ran, so these were clean a moment ago and the sync
        # is the only thing that can have touched them.
        git add -- "${rewritten[@]}"
        printf '\033[32m  ✓ auto-staged regenerated index tables\033[0m\n'
    else
        printf '  Left unstaged: this commit changes no vault content, so the\n'
        printf '  tables it already carries still describe it correctly.\n'
    fi
fi

# --------------------------------------------------------------------- tests
# Only the fast, offline subset. The full suite belongs in CI; a commit hook
# that takes half a minute is a commit hook people disable.
if [[ -d "$SYSTEM_DIR/tests" ]]; then
    printf '→ [pre-commit] Running contract tests...\n'
    if ! output="$(python3 -m unittest discover -s "$SYSTEM_DIR/tests" -t "$SYSTEM_DIR" \
                      -p 'test_contract.py' 2>&1)"; then
        printf '%s\n' "$output" | tail -30
        fail "Contract tests failed." \
             "The agent instructions no longer match the engine."
    fi
fi

# --------------------------------------------------------------- attestation
printf '→ [pre-commit] Running OKF attested computation...\n'
if ! output="$(python3 "$BRAIN" attest 2>&1)"; then
    printf '%s\n' "$output"
    fail "Vault integrity check failed." \
         "Resolve the errors above, then commit again."
fi

# The receipt is written only when the attested content actually changed, so
# this is rare rather than per-commit. On a fresh clone it does not exist yet,
# and an untracked file never shows up in a diff, so test for both.
if (( generating )) \
   && { ! git diff --quiet -- "$RECEIPT" \
        || [[ -n "$(git ls-files --others --exclude-standard -- "$RECEIPT")" ]]; }; then
    printf '\033[33m  ! the attestation receipt changed\033[0m\n'
    git add -- "$RECEIPT"
    printf '\033[32m  ✓ auto-staged updated attestation receipt\033[0m\n'
fi

printf '\033[32m✓ [pre-commit] Vault verified healthy. Proceeding.\033[0m\n'
exit 0
